﻿using System;
using System.Linq.Expressions;
using System.Dynamic;
using Microsoft.Scripting;
using Microsoft.Scripting.Actions;
using Microsoft.Scripting.Runtime;

namespace Cjc.Calculation.ExpressionEngine.Excel.Runtime.Binding
{
	public class ExcelOperationBinder : ExtensionBinaryOperationBinder
	{
		public ExcelBinder Binder { get; private set; }

		private delegate DynamicMetaObject FallbackHandler( DynamicMetaObject target, DynamicMetaObject arg );
		private FallbackHandler handler;
		private ExpressionType operation;

		public ExcelOperationBinder( ExcelBinder binder, ExpressionType operation )
			: base( TranslateOperation( operation ) )
		{
			this.operation = operation;
			this.Binder = binder;
			this.handler = DefaultOperation;

			switch ( operation )
			{
                //case ExpressionType.Concatenate:
                //    {
                //        handler = EnsureString;
                //        break;
                //    }

				case ExpressionType.Add:
					{
						handler = EnsureAddable;
						break;
					}

				case ExpressionType.Subtract:
				case ExpressionType.Multiply:
				case ExpressionType.Divide:
					{
						handler = EnsureNumeric;
						break;
					}

				case ExpressionType.LessThan:
				case ExpressionType.LessThanOrEqual:
				case ExpressionType.GreaterThan:
				case ExpressionType.GreaterThanOrEqual:
					{
						handler = EnsureComparable;
						break;
					}

				case ExpressionType.Equal:
				case ExpressionType.NotEqual:
					{
						handler = EnsureComparable;
						break;
					}
			}
		}

		private static string TranslateOperation( ExpressionType operation )
		{
            //switch ( operation )
            //{
            //    case ExpressionType.Concatenate: return "Add";
            //}

			return operation.ToString();
		}

		public override DynamicMetaObject FallbackBinaryOperation( DynamicMetaObject target, DynamicMetaObject arg, DynamicMetaObject errorSuggestion )
		{
			return handler( target, arg );
		}

		private DynamicMetaObject EnsureAddable( DynamicMetaObject t, DynamicMetaObject a )
		{
			// CJC 03/10/2009 - Behave as Javascript and assume string result if either arg is a string
			return ( t.LimitType == typeof( string ) || a.LimitType == typeof( string ) )
				? EnsureString( t, a )
				: EnsureNumeric( t, a );
/*
			return ( t.LimitType == typeof( string ) )
				? EnsureString( t, a )
				: EnsureNumeric( t, a );*/
		}

		private DynamicMetaObject EnsureString( DynamicMetaObject t, DynamicMetaObject a )
		{
			if ( t.LimitType != typeof( string ) )
			{
				t = Cast( typeof( string ), t );
			}

			return DefaultOperation( t, a );
		}

		private DynamicMetaObject EnsureNumeric( DynamicMetaObject t, DynamicMetaObject a )
		{
			switch ( Type.GetTypeCode( t.LimitType ) )
			{
				case TypeCode.Int32:
				case TypeCode.Int64:
				case TypeCode.Double:
				case TypeCode.Decimal:
					break;

				default:
					t = Cast( typeof( decimal ), t );
					break;
			}

			return DefaultOperation( t, a );
		}

		private DynamicMetaObject EnsureComparable( DynamicMetaObject t, DynamicMetaObject a )
		{
			MakeComparable( ref t, ref a );

			return DefaultOperation( t, a );
		}

		private void MakeComparable( ref DynamicMetaObject t, ref DynamicMetaObject a )
		{
			if ( t.LimitType != a.LimitType )
			{
				var t1Code = Type.GetTypeCode( t.LimitType );
				var t2Code = Type.GetTypeCode( a.LimitType );

				var bestType = (TypeCode)Math.Max( (int)t1Code, (int)t2Code );
				Type type = typeof( object );

				switch( bestType )
				{
					case TypeCode.Int16:
					case TypeCode.Int32:
						type = typeof( int );
						break;

					case TypeCode.Single:
					case TypeCode.Double:
						type = typeof( double );
						break;

					case TypeCode.Decimal:
						type = typeof( decimal );
						break;

					case TypeCode.DateTime:
						type = typeof( DateTime );
						break;

					case TypeCode.String:
						type = typeof( string );
						break;
				}

				t = Cast( type, t );
				a = Cast( type, a );
			}
		}

		private DynamicMetaObject DefaultOperation( DynamicMetaObject target, DynamicMetaObject arg )
		{
            DynamicMetaObject binding = Binder.DoOperation(operation, target, arg);

            // If the expression does not return object, throw in a conversion
            if (!AreReferenceAssignable(typeof(object), binding.Expression.Type))
                binding = binding.BindConvert(new ExcelConversionBinder(Binder, typeof(object), ConversionResultKind.ExplicitCast));

            return binding;
		}

        // This mirrors the check made in the DLR to verify the return type produced by a binding
        internal static bool AreReferenceAssignable(Type dest, Type src)
        {
            if (dest == src)
            {
                return true;
            }
            if (!dest.IsValueType && !src.IsValueType && dest.IsAssignableFrom(src))
            {
                return true;
            }
            return false;
        }

		private DynamicMetaObject Cast( Type toType, DynamicMetaObject value )
		{
			return Binder.ConvertTo( toType, ConversionResultKind.ImplicitCast, value );
		}

		public override string ToString()
		{
			return string.Format( "{0} ({1})", Operation, operation );
		}
	}
}